Skip to content

feat(approvals): typed output pickers, quick-path guard, expression completion (framework#3447, #2829) - #2831

Merged
os-zhuang merged 1 commit into
mainfrom
feat/3447-quickpath-picker-completion
Jul 27, 2026
Merged

feat(approvals): typed output pickers, quick-path guard, expression completion (framework#3447, #2829)#2831
os-zhuang merged 1 commit into
mainfrom
feat/3447-quickpath-picker-completion

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Second follow-up wave for framework#3447 (pairs with framework PR objectstack-ai/objectstack#3554). Closes #2829.

1. Typed decision-output pickers. decision_output_defs ({ key, label?, type, multiple? }, surfaced by fw#3554) renders record pickers in the decide dialog: user → the sys_user people picker, department/position/team → the matching system-object lookup; multiple collects an id array. Bare keys keep the free-text input; older backends (key list only) keep working.

2. Quick-path guard (#2829). Inline a/r, hover buttons, mobile card buttons and bulk apply no longer decide a request whose node declares decision outputs — only the drawer dialog collects those fields. Buttons disable with an explanation ('This approval collects decision outputs — open it to decide.'); bulk selection excludes such rows via the existing actionable-count messaging.

3. Approval-expression completion + inline root validation. The expression approver input gains the scope data-picker with three groups — current.<field> (live at node entry), trigger.<field> (submit snapshot), vars.* (flow variables) — built by useFlowScope from the same materials as the condition picker but with the approval root set (never record.x/bare fields, which the runtime rejects). nodeOutputRefs now models approval nodes (<nodeId>.decision + declared decisionOutputs keys) so the previous stage's outputs are pickable — including the exact vars.lead_review.next_reviewers the headline scenario references — and vars.previous is always listed, fixing a false 'not a reference in scope' warning on flows with no other variables.

Browser-verified (HMR console + framework#3554 worktree backend)

  • Decide dialog: Next Reviewers rendered as a multi-select people picker (search + avatars); picking two users posted an id array; the Co-sign stage's pending_approvers resolved to exactly those two ids; flow completed.
  • Inbox: the outputs-declaring row's hover/mobile approve+reject buttons render disabled with the hint (4 elements verified); bulk/keyboard share the same guard.
  • Designer: the expression input's picker shows all three groups with field labels; the unknown-root false positive is gone (0 warnings).
  • turbo type-check green; app-shell tests 89 green.

🤖 Generated with Claude Code

…ompletion (framework#3447, #2829)

Typed decision_output_defs render record pickers (user people-picker /
department/position/team lookups; multiple → id array). Quick decision
paths (inline a/r, hover, mobile, bulk) are guarded off requests that
declare decision outputs — only the drawer dialog collects them; buttons
disable with an explanation. The expression approver input gains a
three-group scope picker (current/trigger/vars) with inline root
validation; nodeOutputRefs models approval outputs (decision + declared
keys) so the previous stage's outputs are pickable, and vars.previous is
always listed so legitimate vars.* references never flag as unknown.
Browser-verified end to end (picker multi-select → co-sign slate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 27, 2026 4:44am

Request Review

@github-actions github-actions Bot added the apps label Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.0 KB 350 KB
Entry file index-Dc2w60OV.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 21.70KB 4.21KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 33.74KB 8.53KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 1.83KB 0.79KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.86KB 0.85KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 450.70KB 98.15KB
core (index.js) 1.86KB 0.63KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.29KB 31.96KB
fields (index.js) 214.60KB 52.67KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 2.87KB 1.28KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.37KB 12.48KB
plugin-charts (index.js) 46.90KB 13.26KB
plugin-chatbot (index.js) 179.53KB 42.79KB
plugin-dashboard (index.js) 108.71KB 28.00KB
plugin-designer (index.js) 210.92KB 42.69KB
plugin-detail (index.js) 214.78KB 52.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.33KB 39.53KB
plugin-grid (index.js) 176.51KB 46.38KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.71KB 23.32KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.37KB 7.20KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.70KB 20.87KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.97KB 0.93KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit 7f153de into main Jul 27, 2026
14 checks passed
@os-zhuang
os-zhuang deleted the feat/3447-quickpath-picker-completion branch July 27, 2026 04:48
hotlong pushed a commit that referenced this pull request Jul 30, 2026
…#2961)

* fix(approvals): decision outputs reach both decision surfaces (#2955)

An approval node's `decisionOutputs` (framework#3447 P2) never made it to the
approver on either Console surface, though the server has shipped the typed
declaration on the request row all along.

The Approval Center DID synthesize a typed picker per declared output
(#2831), but spelled the picker target `referenceTo`. Every collected param
passes through `resolveActionParams()` first, and its inline branch rebuilds
the param from a fixed key list, reading the target from `reference` — so the
target was dropped and `paramToField()` degraded the targetless picker to a
plain text input labelled "<label> 的记录 ID". `user` outputs were unaffected
(that widget needs no target); `department`/`position`/`team` were all broken.

The record header collected nothing at all: its Approve/Reject shipped their
inputs under `collectParams`, a key nothing reads (ActionRunner collects from
`actionParams`). No dialog had opened there since ADR-0019 — the comment was
silently dropped on every record-page decision, and a node declaring
`decisionOutputs` resumed the flow with `vars.<node>.<key>` missing, so the
next node's `expression` approver faulted or fell through to
`onEmptyApprovers` with no hint to anyone.

The widget mapping now lives in `utils/decisionOutputParams` so the two
surfaces cannot drift again; the header collects through `actionParams`, folds
`outputs.<key>` into the decide body, and its comment box is a real textarea
(the resolver drops `multiline`). Tests pin the round trip through param
resolution — the stage that actually broke — not just the emitted shape.

Verified end to end against the app-showcase runtime: an approval node
declaring a `position`-typed multi-select renders a sys_position picker in
both the Approval Center drawer and the record header, and deciding from the
record page carries the outputs through to `vars.*` — the co-sign node's
`expression` approver resolved to the user picked in the dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(approvals): block a decision on a required output (#2955)

The platform half of this issue gives `decisionOutputs` a `required` flag: the
server rejects an approve that carries no value for one, before any write, so
the run can never resume past the node with the key a downstream `expression`
approver reads still missing.

The console mirrors it where the approver actually is. A required output is
marked required on the param it synthesizes, so the dialog flags the empty
field and refuses Confirm instead of sending a decision the server will 400.
APPROVE only, matching the server: a reject leaves down the reject edge where
nothing reads the outputs, so the reject dialog offers the same fields without
blocking on them — the two dialogs now differ in exactly that flag. Both
decision surfaces get it from the one shared helper, and a backend that
predates the field requires nothing, which is the behaviour of the previous
commit unchanged.

Verified against the showcase runtime (its dynamic-approval flow declares the
flag): the record header's Approve refuses with "Next Reviewers 为必填项" and
sends no request, its Reject offers the field unmarked, the Approval Center
drawer agrees on both, and a filled approve routes the co-sign node to the
picked user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Approvals inbox: quick approve (inline a/r, bulk) silently drops declared decision outputs

1 participant